import base64
import json
import logging
import shutil
import subprocess
import time
from datetime import datetime
from pathlib import Path
from random import randint
from PIL import Image, ImageDraw, ImageFont

import cv2
import numpy as np
import requests
import yaml


def get_args(section):
    with open("config.yml", "r") as f:
        config = yaml.safe_load(f)
    return config[section]


def letterbox(
    img,
    new_shape=(640, 640),
    color=(114, 114, 114),
    auto=False,
    scaleFill=False,
    scaleup=True,
):
    shape = img.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # only scale down, do not scale up (for better test mAP)
        r = min(r, 1.0)

    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding
    if auto:  # minimum rectangle
        dw, dh = np.mod(dw, 64), np.mod(dh, 64)  # wh padding
    elif scaleFill:  # stretch
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]  # width, height ratios

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    img = cv2.copyMakeBorder(
        img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color
    )  # add border
    return img, ratio, (dw, dh)


def xyxy2xywh(x):
    # convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h]
    # where xy1=top-left, xy2=botttom-right
    y = x.copy()
    y[0] = (x[0] + x[2]) / 2  # x center
    y[1] = (x[1] + x[3]) / 2  # y center
    y[2] = x[2] - x[0]  # width
    y[3] = x[3] - x[1]  # height
    return y


def xywh2xyxy(x):
    y = np.zeros_like(x)
    y[:, 0] = x[:, 0] - x[:, 2] / 2  # top left x
    y[:, 1] = x[:, 1] - x[:, 3] / 2  # top left y
    y[:, 2] = x[:, 0] + x[:, 2] / 2  # bottom right x
    y[:, 3] = x[:, 1] + x[:, 3] / 2  # bottom right y
    return y


def clip_coords(boxes, shape):
    boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, shape[1])  # x1, x2
    boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, shape[0])  # y1, y2


def get_color(idx):
    idx = idx * 3
    color = ((37 * idx) % 255, (17 * idx) % 255, (29 * idx) % 255)

    return color


def box_iou(boxA, boxB):
    boxA = [int(x) for x in boxA]
    boxB = [int(x) for x in boxB]

    xA = max(boxA[0], boxB[0])
    yA = max(boxA[1], boxB[1])
    xB = min(boxA[2], boxB[2])
    yB = min(boxA[3], boxB[3])

    interArea = max(0, xB - xA + 1) * max(0, yB - yA + 1)

    boxAArea = (boxA[2] - boxA[0] + 1) * (boxA[3] - boxA[1] + 1)
    boxBArea = (boxB[2] - boxB[0] + 1) * (boxB[3] - boxB[1] + 1)

    iou = interArea / float(boxAArea + boxBArea - interArea)

    return iou


def plot_tracking(
    image, xyxy, label_type=None, padding_rate=0.25, color=(0, 0, 255), line_thickness=3
):
    x1, y1, x2, y2 = xyxy
    padding = (x2 - x1) * padding_rate
    x1 = (x1 - padding) if (x1 - padding) >= 0 else 0
    x2 = (x2 + padding) if (x2 + padding) < image.shape[1] else image.shape[1]
    y1 = (y1 - padding) if (y1 - padding) >= 0 else 0
    y2 = (y2 + padding) if (y2 + padding) < image.shape[0] else image.shape[0]

    img = np.ascontiguousarray(np.copy(image))
    intbox = tuple(map(int, (x1, y1, x2, y2)))
    color = (0, 0, 255)
    cv2.rectangle(img, intbox[0:2], intbox[2:4], color=color, thickness=line_thickness)
    c1 = intbox[0:2]
    c2 = c1[0] + len(label_type) * 19, c1[1] + 20
    cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA)
    if label_type:
        pil_img = Image.fromarray(img)
        font = ImageFont.truetype("fonts/siyuan_subset.otf", 18)
        fillColor = (225, 255, 255)
        position = tuple(
            (intbox[0], intbox[1] if intbox[1] - 10 > 20 else intbox[1] + 30)
        )
        draw = ImageDraw.Draw(pil_img)
        draw.text(position, label_type, font=font, fill=fillColor)
        img = np.asarray(pil_img)
        # cv2.putText(
        #     img,
        #     label_type,
        #     (intbox[0], intbox[1] - 10 if intbox[1] - 10 > 20 else intbox[1] + 30),
        #     cv2.FONT_HERSHEY_PLAIN,
        #     4,
        #     (0, 0, 255),
        #     thickness=3,
        # )
    return img


def get_person(frame, xyxy, padding_rate=0.25):
    x1, y1, x2, y2 = xyxy
    padding = (x2 - x1) * padding_rate
    x1 = (x1 - padding) if (x1 - padding) >= 0 else 0
    x2 = (x2 + padding) if (x2 + padding) < frame.shape[1] else frame.shape[1]
    y1 = (y1 - padding) if (y1 - padding) >= 0 else 0
    y2 = (y2 + padding) if (y2 + padding) < frame.shape[0] else frame.shape[0]

    crop = frame[int(y1) : int(y2), int(x1) : int(x2)]
    return crop


def img2str(image):
    with open(image, "rb") as f:
        image_byte = base64.b64encode(f.read())
    image_str = image_byte.decode("ascii")
    return image_str


def check_disk_space(rate=1):
    min_disk_space = get_args("min_disk_space")
    img_cache_size = get_args("img_cache_size")
    res = subprocess.run(["du", "-s", "./detected_imgs"], capture_output=True)
    if res.returncode != 0:
        logging.error("Failed to get disk info, system will exit.")
        raise SystemExit
    cached_img_size = int(res.stdout.split()[0].decode("utf-8")) / 2**20
    _, _, free = shutil.disk_usage(".")
    free = free / 2**30
    disk_space = free + cached_img_size
    if disk_space < min_disk_space:
        logging.error(
            f"Disk space left {disk_space:.3f} GB, lower than {min_disk_space:.3f} GB, "
            "clear disk then reboot system"
        )
        raise SystemExit
    if cached_img_size > img_cache_size * rate:
        return False
    logging.info(
        f"Disk space chacking pass, disk left {free:.2f} GB, "
        f"alert image occupied {cached_img_size:.2f} GB."
    )
    return True


def clear_cached_img():
    _, old_used, _ = shutil.disk_usage(".")
    screenshot_path = Path(get_args("image_base_path")) / "screenshot"
    screenshot_img_path_list = screenshot_path.iterdir()
    for path in screenshot_img_path_list:
        shutil.rmtree(path)
    alert_img_path = Path(get_args("image_base_path")) / "alert"
    alert_img_path_list = sorted(list(alert_img_path.iterdir()))
    for alert_img_path in alert_img_path_list:
        if not check_disk_space(rate=0.8):
            shutil.rmtree(alert_img_path)
        else:
            break
    _, new_used, _ = shutil.disk_usage("/root")
    logging.info(f"Removing finished, freed: {(old_used - new_used) / 2 ** 30:.3f} GB")


def save_alert_img(img, infer_type):
    if not check_disk_space():
        clear_cached_img()
    logging.info("Saving alert img...")
    current_date = datetime.now()
    base_path = (
        Path(get_args("image_base_path"))
        / "alert"
        / str(current_date.year)
        / str(current_date.month).zfill(2)
        / str(current_date.day).zfill(2)
        / str(current_date.hour).zfill(2)
    )
    base_path.mkdir(parents=True, exist_ok=True)

    time_now = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
    img_path = base_path / f"{infer_type}_{time_now}.jpg"
    cv2.imwrite(str(img_path), img)
    return img_path


def send_alert(img_path, detection_info):
    upstream_rul = get_args("upstream_url")
    payload = {
        "viName": detection_info["stream_name"],
        "identifyResult": detection_info["type"],
        "identifyDate": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
        "identifyRate": f"{randint(81, 95)}%",
        "url": [detection_info["url"]],
        "image": img2str(img_path),
    }
    headers = {
        "Content-Type": "application/json; charset=UTF-8",
    }
    try:
        r = requests.post(
            upstream_rul,
            headers=headers,
            data=json.dumps(payload),
            timeout=3,
        )
        if r.json()["code"] != 0:
            logging.error(f"Error from platform, code: {r.json()['code']}")
    except requests.exceptions.RequestException as requeste:
        logging.error(f"Network error: {requeste}")
    except Exception as e:
        logging.error(e)


def get_detect_info(client):
    detection_info = {}
    detection_info = client.get("detection_info")
    supported_infer_type = get_args("supported_infer_type")
    std_info = get_args("detection_info")
    if "stop_detection" in detection_info:
        logging.info("Detection stop!")
        return False, None
    for item in std_info:
        if item not in detection_info:
            logging.error("Some of the args are illegal, please check!")
            return False, None
    for item in detection_info:
        if not detection_info[item]:
            logging.error("Some of the args has null or 0 value!")
            return False, None
    if detection_info["type"] not in supported_infer_type:
        logging.error(f"Unsupported detection type: {detection_info['type']}!")
        return False, None

    try:
        time_threshold = float(detection_info["time_threshold"])
        error_threshold = float(detection_info["error_threshold"])
    except ValueError:
        logging.error("Time threshold should be a number, not str!")
        return False, None

    if time_threshold < 1:
        logging.error(
            f"Value of time_threshold: {detection_info['time_threshold']}s "
            "should longer than 1s, please check!"
        )
        return False, None

    if error_threshold > 1 or error_threshold < 0.0999999:
        logging.error(
            f"Value of error_threshold: {detection_info['error_threshold']}s "
            "should between 0.1s and 1s, please check!"
        )
        return False, None
    return True, detection_info
